<?php

declare(strict_types=1);

namespace Dreamcat\ApiDoc\ApiGenerators;

use Dreamcat\ApiDoc\AddSimpleVariableHelper\AddCookie;
use Dreamcat\ApiDoc\AddSimpleVariableHelper\AddHeader;
use Dreamcat\ApiDoc\AddSimpleVariableHelper\AddPathVariable;
use Dreamcat\ApiDoc\Annotations\ApiModelProperty;
use Dreamcat\ApiDoc\Annotations\ApiOperation;
use Dreamcat\ApiDoc\Annotations\ControllerTag;
use Dreamcat\ApiDoc\Annotations\EnumDesc;
use Dreamcat\ApiDoc\Annotations\IgnoreParam;
use Dreamcat\ApiDoc\Annotations\MapModelDesc;
use Dreamcat\ApiDoc\ApiDefineGeneratorInterface;
use Dreamcat\ApiDoc\Enums\DataType;
use Dreamcat\ApiDoc\Popo\ApiDefinition;
use Dreamcat\ApiDoc\Popo\BodyDefinition;
use Dreamcat\ApiDoc\Popo\RouterEntranceDefinition;
use Dreamcat\ApiDoc\Utils\EnumUtils;
use DreamCat\Array2Class\Annotation\ObjectAttribute;
use DreamCat\ControllerParamGen\Annotation\CtlParamType;
use DreamCat\ObjectValid\Annotation\NumberScope;
use DreamCat\ObjectValid\Annotation\Pattern;
use DreamCat\ObjectValid\Annotation\Size;
use DreamCat\ObjectValid\Utils\AnnotationUtils;
use Dreamcat\PropertyAnalysis\Annotation\ArrayDescAnno;
use Dreamcat\PropertyAnalysis\Pojo\BuildinType;
use Dreamcat\PropertyAnalysis\PropertyAnalysis;
use Dreamcat\PropertyAnalysis\PropertyAnalysisInterface;
use MyCLabs\Enum\Enum;
use Psr\Http\Message\RequestInterface;
use ReflectionClass;
use ReflectionEnum;
use ReflectionFunctionAbstract;
use ReflectionMethod;
use ReflectionNamedType;
use ReflectionParameter;
use ReflectionProperty;
use ReflectionType;
use ReflectionUnionType;
use RuntimeException;
use stdClass;

/**
 * 接口文档生成器
 * @author vijay
 */
class ApiDocGenerator implements ApiDefineGeneratorInterface
{
    /** @var PropertyAnalysisInterface 类的属性分析器 */
    private PropertyAnalysisInterface $propertyAnalysis;

    /** @var string[] 忽略的自动类型的类或接口 */
    private array $ignoreTypes = [RequestInterface::class => 1];

    /**
     * @return string[] 忽略的自动类型的类或接口
     */
    public function getIgnoreTypes(): array
    {
        return array_keys($this->ignoreTypes);
    }

    /**
     * @return static 对象本身
     */
    public function clearIgnoreTypes(): ApiDocGenerator
    {
        $this->ignoreTypes = [];
        return $this;
    }

    /**
     * 添加忽略的自动类型的类或接口
     * @param string $ignoreType
     * @return $this
     */
    public function addIgnoreType(string $ignoreType): ApiDocGenerator
    {
        $this->ignoreTypes[$ignoreType] = 1;
        return $this;
    }

    /** @inheritDoc */
    public function parseApi(RouterEntranceDefinition $routerEntranceDefinition): ApiDefinition
    {
        $apiDefinition = new ApiDefinition();
        $apiDefinition->setMethod($routerEntranceDefinition->getMethod())
            ->setUri($routerEntranceDefinition->getUri());
        return $this->parseApiByReflection($routerEntranceDefinition->getAction(), $apiDefinition);
    }

    /**
     * 通过反射获取接口文档
     * @param ReflectionMethod $reflectionMethod 方法反射
     * @param ApiDefinition $apiDefinition 已设置的接口定义
     * @return ApiDefinition 完善的接口定义
     */
    protected function parseApiByReflection(
        ReflectionFunctionAbstract $reflectionMethod,
        ApiDefinition $apiDefinition
    ): ApiDefinition {
        $apiDefinition = $this->parseApiByApiInfo($apiDefinition, $reflectionMethod);
        # 取出方法的参数和返回值定义
        $apiDefinition = $this->parseApiByReturn($apiDefinition, $reflectionMethod);
        return $this->parseApiByParams($reflectionMethod->getParameters(), $apiDefinition);
    }

    /**
     * 解析控制器方法上的注解来生成文档
     * @param ApiDefinition $apiDefinition 已定义的接口文档
     * @param ReflectionFunctionAbstract $reflectionMethod 控制器方法反射
     * @return ApiDefinition 完善的接口文档
     */
    protected function parseApiByApiInfo(
        ApiDefinition $apiDefinition,
        ReflectionFunctionAbstract $reflectionMethod
    ): ApiDefinition {
        $apiInfo = AnnotationUtils::getAttrObj($reflectionMethod, ApiOperation::class);
        if ($apiInfo) {
            $apiDefinition->setName($apiInfo->getTitle())
                ->setApiTag($apiInfo->getTag())
                ->setDesc($apiInfo->getDesc());
        } else {
            $apiDefinition->setName($apiDefinition->getUri())->setApiTag("")->setDesc("");
        }
        if (
            $apiDefinition->getApiTag() === ""
            && method_exists($reflectionMethod, "getDeclaringClass")
            && $reflectionMethod->getDeclaringClass()
        ) {
            $info = AnnotationUtils::getAttrObj($reflectionMethod->getDeclaringClass(), ControllerTag::class);
            if ($info) {
                $apiDefinition->setApiTag($info->getTag());
            }
        }
        return $apiDefinition;
    }

    /**
     * 通过控制器反射值
     * @param ApiDefinition $apiDefinition 已定义的接口文档
     * @param ReflectionFunctionAbstract $reflectionMethod 控制器方法反射
     * @return ApiDefinition 完善的接口文档
     */
    protected function parseApiByReturn(
        ApiDefinition $apiDefinition,
        ReflectionFunctionAbstract $reflectionMethod
    ): ApiDefinition {
        $returnType = $reflectionMethod->getReturnType();
        if ($returnType) {
            if ($returnType instanceof ReflectionNamedType && $returnType->getName() == "void") {
                $return = null;
            } else {
                $return = $this->getBodyDefByRefType($reflectionMethod->getReturnType(), new BodyDefinition());
            }
        } else {
            $return = new BodyDefinition();
            $return->setDataType(DataType::UNKNOWN());
        }
        $apiDefinition->setResponseDefinition($return);
        return $apiDefinition;
    }

    /**
     * 通过解析参数或返回值的类型描述完善接口文档
     * @param ReflectionType|null $reflectionType 类型描述
     * @param BodyDefinition $bodyDefinition 已定义的参数体
     * @param ReflectionProperty|ReflectionParameter|null $reflectionInfo 参数反射或对象的属性反射
     * @return BodyDefinition 完善的参数体定义
     */
    protected function getBodyDefByRefType(
        ?ReflectionType $reflectionType,
        BodyDefinition $bodyDefinition,
        ReflectionProperty|ReflectionParameter $reflectionInfo = null
    ): BodyDefinition {
        if (!$reflectionType) {
            return $bodyDefinition;
        }
        $bodyDefinition->setIsRequired($bodyDefinition->isRequired() || !$reflectionType->allowsNull());
        if ($reflectionType instanceof ReflectionNamedType) {
            $bodyDefinition = $this->getBodyDefByTypeName(
                $reflectionType->getName(),
                $bodyDefinition,
                $reflectionInfo
            );
            // @codeCoverageIgnoreStart
        } elseif ($reflectionType instanceof ReflectionUnionType) {
            foreach ($reflectionType->getTypes() as $reflectionNamedType) {
                $bodyDefinition = $this->getBodyDefByTypeName(
                    $reflectionNamedType->getName(),
                    $bodyDefinition,
                    $reflectionInfo
                );
            }
            // @codeCoverageIgnoreEnd
        } else {
            // @codeCoverageIgnoreStart
            throw new RuntimeException("未知的 ReflectionType 子类 " . get_class($reflectionType));
            // @codeCoverageIgnoreEnd
        }
        return $bodyDefinition;
    }

    /**
     * 参数类型字符串解析完善文档
     * @param string $typeName 类型字符串
     * @param BodyDefinition $bodyDefinition 已定义的参数体
     * @param ReflectionProperty|ReflectionParameter|null $reflectionInfo 参数反射或对象的属性反射
     * @return BodyDefinition 完善的参数体定义
     * @noinspection PhpDocMissingThrowsInspection
     */
    protected function getBodyDefByTypeName(
        string $typeName,
        BodyDefinition $bodyDefinition,
        ReflectionProperty|ReflectionParameter $reflectionInfo = null
    ): BodyDefinition {
        if ($reflectionInfo) {
            $info = AnnotationUtils::getAttrObj($reflectionInfo, ApiModelProperty::class);
            if ($info) {
                $bodyDefinition->setDesc($info->getTitle())
                    ->setMock($info->getMock())
                    ->setExample($info->getExample());
            }
            if (
                $reflectionInfo instanceof ReflectionParameter && $reflectionInfo->isOptional()
                || $reflectionInfo instanceof ReflectionProperty && $reflectionInfo->hasDefaultValue()
            ) {
                /** @noinspection PhpUnhandledExceptionInspection */
                $bodyDefinition->setDefault($reflectionInfo->getDefaultValue())
                    ->setIsRequired(false);
            }
        }
        if ($reflectionInfo instanceof ReflectionParameter) {
            $bodyDefinition->setIsRequired($bodyDefinition->isRequired() || !$reflectionInfo->allowsNull());
        }
        switch ($typeName) {
            case BuildinType::STRING:
                $bodyDefinition->setDataType(DataType::STRING());
                if (!$reflectionInfo) {
                    break;
                }
                $bodyDefinition->setSize(AnnotationUtils::getAttrObj($reflectionInfo, Size::class));
                $bodyDefinition->setPattern(AnnotationUtils::getAttrObj($reflectionInfo, Pattern::class));
                break;
            case BuildinType::INT:
            case BuildinType::FLOAT:
                $bodyDefinition->setDataType(DataType::from($typeName));
                if (!$reflectionInfo) {
                    break;
                }
                $bodyDefinition->setNumberScope(AnnotationUtils::getAttrObj($reflectionInfo, NumberScope::class));
                break;
            case BuildinType::BOOL:
                $bodyDefinition->setDataType(DataType::BOOL());
                break;
            case "null":
                $bodyDefinition->setDataType(DataType::NULL());
                break;
            case "mixed":
                $bodyDefinition->setDataType(DataType::UNKNOWN());
                break;
            case BuildinType::ARRAY:
                if (!$reflectionInfo) {
                    $bodyDefinition->setDataType(DataType::ARRAY());
                    break;
                }
                $bodyDefinition->setSize(AnnotationUtils::getAttrObj($reflectionInfo, Size::class));
                $info = AnnotationUtils::getAttrObj($reflectionInfo, MapModelDesc::class);
                if ($info) {
                    $bodyDefinition->setDataType(DataType::MAP())
                        ->setMapKeyDesc($info->getKeyDesc())
                        ->setMapValueDesc($info->getValueDesc());
                } else {
                    $bodyDefinition->setDataType(DataType::ARRAY());
                }
                $info = AnnotationUtils::getAttrObj($reflectionInfo, ArrayDescAnno::class);
                if (!$info) {
                    break;
                }
                $child = $this->getBodyDefByTypeName($info->getElementType(), new BodyDefinition());
                $bodyDefinition->setChildDefinition($child);
                break;
            case stdClass::class:
                $bodyDefinition->setDataType(DataType::OBJECT());
                break;
            default:
                if (class_exists($typeName)) {
                    $class = new ReflectionClass($typeName);
                    if (EnumUtils::isEnumClass($class)) {
                        # 枚举
                        $bodyDefinition = $this->formatEnumDef($class, $bodyDefinition);
                    } else {
                        $bodyDefinition = $this->formatNormalClass($class, $bodyDefinition);
                    }
                } else {
                    $bodyDefinition->setDataType(DataType::UNKNOWN());
                    $desc = "无法解析的类型 {$typeName}";
                    if (strlen($bodyDefinition->getDesc())) {
                        $desc = "{$bodyDefinition->getDesc()}; {$desc}";
                    }
                    $bodyDefinition->setDesc($desc);
                }
        }
        return $bodyDefinition;
    }

    /**
     * 格式化枚举值的定义
     * @param ReflectionClass $class 枚举类的反射 Enum子类
     * @param BodyDefinition $bodyDefinition 已定义的参数体
     * @return BodyDefinition 完善的参数体定义
     */
    protected function formatEnumDef(ReflectionClass $class, BodyDefinition $bodyDefinition): BodyDefinition
    {
        $bodyDefinition->setDataType(DataType::ENUM());
        if ($class->isSubclassOf(Enum::class)) {
            foreach ($class->getReflectionConstants() as $constant) {
                $desc = AnnotationUtils::getAttrObj($constant, EnumDesc::class);
                if ($desc && strlen($desc->getTitle())) {
                    $bodyDefinition->addEnumDesc(strval($constant->getValue()), $desc->getTitle());
                } else {
                    $bodyDefinition->addEnumDesc(strval($constant->getValue()), $constant->getName());
                }
            }
        } else {
            /** @noinspection PhpUnhandledExceptionInspection */
            $ref = new ReflectionEnum($class->getName());
            foreach ($ref->getCases() as $case) {
                if ($ref->isBacked()) {
                    $val = $case->getBackingValue();
                } else {
                    $val = $case->getName();
                }
                $desc = AnnotationUtils::getAttrObj($case, EnumDesc::class);
                if ($desc && strlen($desc->getTitle())) {
                    $bodyDefinition->addEnumDesc(strval($val), $desc->getTitle());
                } else {
                    $bodyDefinition->addEnumDesc(strval($val), $case->getName());
                }
            }
        }
        return $bodyDefinition;
    }

    /**
     * 格式化常规类的定义
     * @param ReflectionClass $class 类反射
     * @param BodyDefinition $bodyDefinition 已定义的参数体
     * @return BodyDefinition 完善的参数体定义
     */
    protected function formatNormalClass(ReflectionClass $class, BodyDefinition $bodyDefinition): BodyDefinition
    {
        $bodyDefinition->setDataType(DataType::OBJECT());
        $list = $this->getPropertyAnalysis()->analysis($class)->getPropertyList();
        foreach ($list as $field => $propertyResult) {
            $sub = new BodyDefinition();
            $ref = $propertyResult->getProperty();
            $info = AnnotationUtils::getAttrObj($ref, ObjectAttribute::class);
            if ($info && $info->isIgnore()) {
                continue;
            }
            $sub->setField($info?->getAlias() ?? $field);
            $sub = $this->getBodyDefByRefType($ref->getType(), $sub, $ref);
            $sub->setIsRequired(!$ref->getType()->allowsNull());
            $bodyDefinition->addObjectProperty($sub);
        }
        return $bodyDefinition;
    }

    /**
     * @return PropertyAnalysisInterface 类的属性分析器
     */
    public function getPropertyAnalysis(): PropertyAnalysisInterface
    {
        if (!isset($this->propertyAnalysis)) {
            $this->setPropertyAnalysis(new PropertyAnalysis());
        }
        return $this->propertyAnalysis;
    }

    /**
     * @param PropertyAnalysisInterface $propertyAnalysis 类的属性分析器
     */
    public function setPropertyAnalysis(PropertyAnalysisInterface $propertyAnalysis): void
    {
        $this->propertyAnalysis = $propertyAnalysis;
    }

    /**
     * 通过函数参数解析文档
     * @param ReflectionParameter[] $params 函数的参数列表
     * @param ApiDefinition $apiDefinition 已设置的接口定义
     * @return ApiDefinition 完善的接口定义
     */
    protected function parseApiByParams(array $params, ApiDefinition $apiDefinition): ApiDefinition
    {
        # 从uri中解析有哪些pathVariable
        $preg = "#{(?<name>\\w+)}#";
        if (preg_match_all($preg, $apiDefinition->getUri(), $matches)) {
            $matches = array_flip($matches["name"]);
        } else {
            $matches = [];
        }
        # 遍历解析参数
        foreach ($params as $param) {
            if (AnnotationUtils::getAttrObj($param, IgnoreParam::class)) {
                # 忽略的参数
                continue;
            }
            $info = AnnotationUtils::getAttrObj($param, CtlParamType::class);
            $apiDefinition = match ($info?->getType()) {
                CtlParamType::TYPE_REQUEST_BODY => $this->formatBody($param, $apiDefinition, $info->getPath()),
                CtlParamType::TYPE_PATH_VARIABLE => $this->formatPathVariable(
                    $param,
                    $apiDefinition,
                    $info->getPath(),
                    $matches
                ),
                CtlParamType::TYPE_GET_PARAM => $this->formatGet($param, $apiDefinition, $info->getPath()),
                CtlParamType::TYPE_HEADER => $this->formatHeader($param, $apiDefinition, $info->getPath()),
                CtlParamType::TYPE_COOKIE => $this->formatCookie($param, $apiDefinition, $info->getPath()),
                default => $this->formatAutoParater($param, $apiDefinition, $matches)
            };
        }
        return $apiDefinition;
    }

    /**
     * 格式化请求体
     * @param ReflectionParameter $parameter 请求体代表的控制器参数反射
     * @param ApiDefinition $apiDefinition 已定义的接口文档
     * @param string|null $path 路径
     * @return ApiDefinition 完善后的接口文档
     */
    protected function formatBody(
        ReflectionParameter $parameter,
        ApiDefinition $apiDefinition,
        ?string $path
    ): ApiDefinition {
        $body = $this->getBodyDefByRefType($parameter->getType(), new BodyDefinition(), $parameter);
        if ($path !== null) {
            $root = $apiDefinition->getBodyDefinition() ?: new BodyDefinition();
            $root->setDataType(DataType::OBJECT());
            $apiDefinition->setBodyDefinition($root);
            $ary = explode("/", $path);
            $root->addDeepChild($ary, $body);
        } else {
            $apiDefinition->setBodyDefinition($body);
        }
        return $apiDefinition;
    }

    /**
     * 格式化路径变量
     * @param ReflectionParameter $parameter 请求参数代表的控制器参数反射
     * @param ApiDefinition $apiDefinition 已定义的接口文档
     * @param string|null $path 路径
     * @param array $pathVariables uri中定义的路径变量
     * @return ApiDefinition 完善后的接口文档
     */
    protected function formatPathVariable(
        ReflectionParameter $parameter,
        ApiDefinition $apiDefinition,
        ?string $path,
        array $pathVariables
    ): ApiDefinition {
        $body = $this->getBodyDefByRefType($parameter->getType(), new BodyDefinition(), $parameter);
        return (new AddPathVariable($pathVariables))->addSimpleVariable($parameter, $body, $apiDefinition, $path);
    }

    /**
     * 格式化get参数
     * @param ReflectionParameter $parameter 请求参数代表的控制器参数反射
     * @param ApiDefinition $apiDefinition 已定义的接口文档
     * @param string|null $path 路径
     * @return ApiDefinition 完善后的接口文档
     */
    protected function formatGet(
        ReflectionParameter $parameter,
        ApiDefinition $apiDefinition,
        ?string $path
    ): ApiDefinition {
        $body = $this->getBodyDefByRefType($parameter->getType(), new BodyDefinition(), $parameter);
        switch ($body->getDataType()->getValue()) {
            case DataType::OBJECT:
            case DataType::MAP:
                break;
            default:
                if ($path === null) {
                    $path = $parameter->name;
                }
        }
        if ($path === null) {
            foreach ($body->getObjectProperties() ?? [] as $item) {
                $apiDefinition->addGetDefinition($item);
            }
        } else {
            $body->setField($this->formatUrlPath($path));
            $apiDefinition->addGetDefinition($body);
        }
        return $apiDefinition;
    }

    /**
     * 格式化url的路径
     * @param string $path 原始的路径定义
     * @return string
     */
    protected function formatUrlPath(string $path): string
    {
        $ary = explode("/", $path);
        $head = array_shift($ary);
        foreach ($ary as $item) {
            $head .= "[{$item}]";
        }
        return $head;
    }

    /**
     * 格式化header参数
     * @param ReflectionParameter $parameter 请求参数代表的控制器参数反射
     * @param ApiDefinition $apiDefinition 已定义的接口文档
     * @param string|null $path 路径
     * @return ApiDefinition 完善后的接口文档
     */
    protected function formatHeader(
        ReflectionParameter $parameter,
        ApiDefinition $apiDefinition,
        ?string $path
    ): ApiDefinition {
        $body = $this->getBodyDefByRefType($parameter->getType(), new BodyDefinition(), $parameter);
        return (new AddHeader())->addSimpleVariable($parameter, $body, $apiDefinition, $path);
    }

    /**
     * 格式化cookie参数
     * @param ReflectionParameter $parameter 请求参数代表的控制器参数反射
     * @param ApiDefinition $apiDefinition 已定义的接口文档
     * @param string|null $path 路径
     * @return ApiDefinition 完善后的接口文档
     */
    protected function formatCookie(
        ReflectionParameter $parameter,
        ApiDefinition $apiDefinition,
        ?string $path
    ): ApiDefinition {
        $body = $this->getBodyDefByRefType($parameter->getType(), new BodyDefinition(), $parameter);
        return (new AddCookie())->addSimpleVariable($parameter, $body, $apiDefinition, $path);
    }

    /**
     * 格式化未加注解的参数
     * @param ReflectionParameter $parameter 请求参数代表的控制器参数反射
     * @param ApiDefinition $apiDefinition 已定义的接口文档
     * @param array $pathVariables uri中定义的路径变量
     * @return ApiDefinition 完善后的接口文档
     */
    protected function formatAutoParater(
        ReflectionParameter $parameter,
        ApiDefinition $apiDefinition,
        array $pathVariables
    ): ApiDefinition {
        if (isset($pathVariables[$parameter->name])) {
            return $this->formatPathVariable($parameter, $apiDefinition, $parameter->name, $pathVariables);
        }
        $type = $parameter->getType();
        if ($type instanceof ReflectionUnionType) {
            // @codeCoverageIgnoreStart
            foreach ($type->getTypes() as $namedType) {
                $name = $namedType->getName();
                if ($this->isIgnoreType($name)) {
                    return $apiDefinition;
                }
            }
            // @codeCoverageIgnoreEnd
        } elseif ($type instanceof ReflectionNamedType) {
            $name = $type->getName();
            if ($this->isIgnoreType($name)) {
                return $apiDefinition;
            }
        }
        return $this->formatGet($parameter, $apiDefinition, $parameter->name);
    }

    /**
     * 判断是否忽略的类型
     * @param string $typeName 类型名称
     * @return bool 是否忽略的类型
     */
    protected function isIgnoreType(string $typeName): bool
    {
        if (isset($this->ignoreTypes[$typeName])) {
            return true;
        }
        if (!class_exists($typeName) && !interface_exists($typeName)) {
            return false;
        }
        foreach ($this->ignoreTypes as $ignoreType => $i) {
            if (is_subclass_of($typeName, $ignoreType)) {
                return true;
            }
        }
        return false;
    }
}

# end of file
